上面這張圖是流程規劃說明裡面畫的Develop CI Pipeline流程,我們已經在前兩篇完成了Build(Code)、Build Image,剩下最後一步就是Deploy Dev環境,這一篇就來完成這最後一步吧!
trigger:
- none
pool:
vmImage: ubuntu-latest
resources:
repositories:
- repository: sources
type: git
name: ironman2022/NetApp
ref: Develop
trigger:
branches:
include:
- Develop
variables:
pipelineArtifact: output
buildResultZipName: buildResult.zip
slnOrCsprojName: IronmanWeb.sln
imgRepository: 'asia-east1-docker.pkg.dev/feisty-mechanic-363012/ironman2022/ironmanweb'
buildDockerfile: 'Dockerfile'
imgRegistryService: 'GCPArtifactRegistry'
cloudRunServiceName: ironmanweb
cloudRunPort: 8080
cloudRunRegion: asia-east1
cloudRunProjectId: feisty-mechanic-363012
gcpAuthJsonFile: ironman2022-gcp-key.json
jobs:
- job: BuildCode
steps:
- checkout: sources
clean: true
- script: |
export UID=$(id -u)
export GID=$(id -g)
docker run --user $UID:$GID --rm \
-v $(Build.SourcesDirectory):/tmp/source \
-v $(Build.BinariesDirectory):/tmp/publish \
-e DOTNET_CLI_HOME=/tmp/.dotnet \
mcr.microsoft.com/dotnet/sdk:6.0-alpine \
dotnet publish /tmp/source/$(slnOrCsprojName) \
-c release \
-o /tmp/publish
displayName: Dotnet Build
- task: ArchiveFiles@2
displayName: 壓縮成zip
inputs:
rootFolderOrFile: $(Build.BinariesDirectory)
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/zipFiles/$(buildResultZipName)'
replaceExistingArchive: true
- task: PublishBuildArtifacts@1
displayName: 上傳到Pipeline Artifact
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)/zipFiles/$(buildResultZipName)'
ArtifactName: '$(pipelineArtifact)'
publishLocation: 'Container'
- job: BuildImage
dependsOn: BuildCode
steps:
- task: DownloadBuildArtifacts@0
displayName: 下載Pipeline Artifact
inputs:
buildType: 'current'
cleanDestinationFolder: true
downloadType: 'single'
artifactName: '$(pipelineArtifact)'
downloadPath: '$(System.ArtifactsDirectory)/'
- task: ExtractFiles@1
displayName: Unzip zip
inputs:
archiveFilePatterns: '$(System.ArtifactsDirectory)/$(pipelineArtifact)/$(buildResultZipName)'
destinationFolder: '$(System.ArtifactsDirectory)/BuildImage'
cleanDestinationFolder: true
overwriteExistingFiles: true
- task: Docker@2
displayName: Build image
inputs:
repository: '$(imgRepository)'
command: 'build'
Dockerfile: $(buildDockerfile)
buildContext: '$(System.ArtifactsDirectory)/BuildImage'
arguments: '--no-cache'
tags: |
latest
- task: Docker@2
displayName: "Login to Container Registry"
inputs:
command: login
containerRegistry: $(imgRegistryService)
- task: Bash@3
displayName: Push docker image
inputs:
targetType: 'inline'
script: |
docker push -a $(imgRepository)
- job: DeployCloudRun
dependsOn: BuildImage
steps:
- task: Bash@3
displayName: Deploy docker image to cloudrun
inputs:
targetType: 'inline'
script: |
docker run --rm \
-v $(Build.SourcesDirectory)/$(gcpAuthJsonFile):/gcp/cloudKey.json \
asia.gcr.io/google.com/cloudsdktool/google-cloud-cli:latest \
bash -c "gcloud auth login --cred-file=/gcp/cloudKey.json && gcloud run deploy $(cloudRunServiceName) --set-env-vars=Ironman=$(Build.BuildId) --image $(imgRepository) --region $(cloudRunRegion) --project $(cloudRunProjectId) --allow-unauthenticated"
哇!一來就是一長串的YAML內容…
不不不,你如果是用VSCode打開,幾乎可以把前面兩個Job折疊起來,這邊增加的DeployCloudRun Job也只有一個Bash的task,不多的。
就讓我娓娓道來這篇主要增加的內容吧!
cloudRunServiceName: ironmanweb
cloudRunPort: 8080
cloudRunRegion: asia-east1
cloudRunProjectId: feisty-mechanic-363012
gcpAuthJsonFile: ironman2022-gcp-key.json
cloudRunServiceName就是圖中Cloud Run的名稱。
cloudRunPort設定為8080是在appsettings.json中設定了Kestrel的Http是使用8080 Port,也就是container內會監聽什麼Port,對應docker指令就是-p 80:8080。
cloudRunRegion則是CloudRun佈署的區域(機房)。
cloudRunProjectId可以直接從Google Cloud管理介面的URL得知,也就是在上圖畫面的時候,看一下瀏覽器上的網址列,「&project=」後面的就是了,或是選擇Project的下拉選單:
最後的gcpAuthJsonFile則是前面幾篇用來授權的Json檔案。不過這邊要補充一下,那時候在新增服務帳戶的時候還少加了一個「服務帳戶使用者」角色,所以漏加這個角色繼續做下去的話,就會碰到下面的錯誤訊息:
PERMISSION_DENIED: Permission ‘iam.serviceaccounts.actAs’ denied on service account
為了讓後面使用gcloud cli可以順利執行,所以要先在IAM裡面將前面新增的服務帳戶加上「服務帳戶使用者」角色:
增加角色之後不需要重新下載用於授權的Json檔案,因為有什麼角色權限不會寫在檔案裡。
Job越加越多,這三個Job之間其實是有相依性的,也就是說要先BuildCode之後才能夠BuildImage,接下來才能DeployCloudRun,所以在第二個和第三個Job底下分別要加上dependsOn的屬性:
- job: BuildImage
dependsOn: BuildCode
- job: DeployCloudRun
dependsOn: BuildImage
這個部份滿重要的,尤其是如果有額外購買CloudAgent的執行數量時,因為Job可以在不同的Agent執行,所以它可以在同一個Pipeline同時跑多個Job(沒有設定相依的dependsOn時),就算沒有額外購買CloudAgent的執行數量,沒設定dependsOn也無法保證它們的執行順序。BuildImage的Job相較前一篇有增加的也只有dependsOn這個屬性。
最後就是DeployCloudRun這個Job,裡面的內容也只有一個Bash的task,所以下面我直接貼bash script的部份:
docker run --rm \
-v $(Build.SourcesDirectory)/$(gcpAuthJsonFile):/gcp/cloudKey.json \
asia.gcr.io/google.com/cloudsdktool/google-cloud-cli:latest \
bash -c "gcloud auth login --cred-file=/gcp/cloudKey.json && gcloud run deploy $(cloudRunServiceName) --set-env-vars=Ironman=$(Build.BuildId) --image $(imgRepository) --region $(cloudRunRegion) --project $(cloudRunProjectId) --allow-unauthenticated"
在這裡是使用google的gcloud CLI工具來執行CloudRun的佈署,不過gcloud CLI工具還是需要安裝的,要嘛是需要先裝在Agent的電腦內,而且還要去爬官方的安裝文件知道怎麼安裝,不然就是安裝在Docker Image裡面。
我們使用的是ClougAgent,所以Agent的環境不是我們可以控制的,每次執行也是新的vm執行起來,所以選擇後者使用google建立的gcloud CLI的Docker Image會是最理想的選擇,除了從Docker Hub可以找到之外,官方文件也有提供不同Container Registry的選擇說明。
使用Container來執行gcloud CLI,我們可以省去安裝的麻煩事,只要會使用就可以了,這讓我們可以更專注在其它的設計部份。
script中的重點只有第二行和最後一行,分別是把授權用的Json檔案關聯到Container裡面,以便讓裡面的gcloud CLI工具可以讀取到內容進行login動作,以及最後一行包含lgoin的指令。
最後一行的指令有個重點,就是我們必須先使用gloud auth login的指令讓CLI工具登入,接著才能執行CloudRun的Deploy指令,也就是說要執行的指令有兩個,所以使用了「&&」這個管道符號讓它接續執行,但是直接這樣接在Image Repository後面是行不通的,「&&」符號的前面會跟最前面的docker run指令合起來作為第一個指令,後面的則是host環境接續docker run指令執行的第二個指令。
所以在這個地方必須是讓docker run執行起來的contianer是執行bash程式,後面接著要執行的指令字串(用引號包起來),也因為是一整個字串,所以沒辦法使用「\」換行,就會是一行很長的指令,下面為了方便閱讀,把它們拆開來說明。
gcloud auth login --cred-file=/gcp/cloudKey.json
「&&」符號前的這一行是將gcloud CLI工具登入,使用–cred-file參數帶入Json檔案,後面的路徑是Container內的路徑,也就是前面-v設定的部份。
gcloud run deploy $(cloudRunServiceName) --set-env-vars=Ironman=$(Build.BuildId) --image $(imgRepository) --region $(cloudRunRegion) --project $(cloudRunProjectId) --allow-unauthenticated
「&&」符號後面的指令我依參數拆行來看應該就很清楚,因為大部份都是上面設定的變數,已經有說明過了。
–set-env-vars的參數是設定CloudRun放入的環境變數(還記得前面的Ironman環境變數嗎?),–allow-unauthenticated則是允許訪客瀏覽,不然CloudRun可能不會正常回應頁面。
gcloud run deploy $(cloudRunServiceName)
–set-env-vars=Ironman=$(Build.BuildId)
–image $(imgRepository)
–region $(cloudRunRegion)
–project $(cloudRunProjectId)
–allow-unauthenticated
關於CloudRun在gcloud CLI可以設定的更多參數部份,請參考官方文件的頁面,之後的文章還會把部份參數用上。
最後,在CI Pipeline成功執行完之後,就可以在對應的Task log中看到gcloud CLI工具吐出來的CloudRun網址,這樣就不用進入到GCP的管理介面去查看。
最後補充一個小細節,就是在這三個Job之中,只有BuildCode這個Job中有明確的加上checkout: sources,並且身份驗證的Json檔案是放在Pipelines這個Git Repository裡面,但是在後面的BuildImage和DeployCloudRun並沒有明確加上checkout動作卻會(可以)取得Pipelines這個Git Repository裡面的檔案,也沒有在resources.repositories底下設定Pipelines,主要是因為這裡的Pipeline YAML檔案就是放在Pipelines這個Git Repository裡面,所以隱含了checkout: self這個動作,替我們省下了一些設定。(下圖紅框與藍框的差異)